/*******************************************************************************
 * Copyright (c) 2002, 2003 David Corbin and others. All rights reserved. This
 * program and the accompanying materials are made available under the terms of
 * the Common Public License v0.5 which accompanies this distribution, and is
 * available at http://www.eclipse.org/legal/cpl-v05.html
 *
 * Contributors: David Corbin - initial implementation - currently delegates to
 * PerforceUtil - add moveFile method and several supporting routines Boris
 * Pruessmann - Changes to determineFolderPosition, improved moveFile, cleanup
 * Boris Pruessmann - Changed signature of reopen to support IFileType.
 ******************************************************************************/
package net.sourceforge.perforce.core.internal.api;

import com.google.common.base.Preconditions;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;

import net.sourceforge.perforce.api.CharSetEnum;
import net.sourceforge.perforce.api.IPerforceCommand;
import net.sourceforge.perforce.core.ISubmitValidator;
import net.sourceforge.perforce.core.PerforceCorePlugin;
import net.sourceforge.perforce.core.PerforceException;
import net.sourceforge.perforce.core.api.IChangeResult;
import net.sourceforge.perforce.core.api.IChangesResult;
import net.sourceforge.perforce.core.api.IClientForm;
import net.sourceforge.perforce.core.api.ICompositeSubmitResult;
import net.sourceforge.perforce.core.api.ICountersResult;
import net.sourceforge.perforce.core.api.IDescribeResult;
import net.sourceforge.perforce.core.api.IFileType;
import net.sourceforge.perforce.core.api.IInfoResult;
import net.sourceforge.perforce.core.api.ILogEntry;
import net.sourceforge.perforce.core.api.IOpenedResult;
import net.sourceforge.perforce.core.api.IPerforceServer;
import net.sourceforge.perforce.core.api.IResolveResult;
import net.sourceforge.perforce.core.api.IResolvedResult;
import net.sourceforge.perforce.core.api.ISubmitResult;
import net.sourceforge.perforce.core.api.IWhereResult;
import net.sourceforge.perforce.core.api.ResolveTypeEnum;
import net.sourceforge.perforce.core.internal.Policy;
import net.sourceforge.perforce.core.internal.model.P4Label;
import net.sourceforge.perforce.core.internal.utils.Profiler;
import net.sourceforge.perforce.core.resources.IChangelist;
import net.sourceforge.perforce.core.resources.LocalFile;
import net.sourceforge.perforce.core.resources.LocalResource;
import net.sourceforge.perforce.core.resources.RemoteFile;
import net.sourceforge.perforce.core.resources.RemoteFolder;
import net.sourceforge.perforce.core.resources.RemoteResource;
import net.sourceforge.perforce.core.syncinfo.FolderSyncInfo;
import net.sourceforge.perforce.core.syncinfo.ResourceSyncInfo;

import org.eclipse.core.filesystem.EFS;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.resources.IWorkspaceRoot;
import org.eclipse.core.resources.team.IResourceTree;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.MultiStatus;
import org.eclipse.core.runtime.NullProgressMonitor;
import org.eclipse.core.runtime.Path;
import org.eclipse.core.runtime.Platform;
import org.eclipse.core.runtime.PlatformObject;
import org.eclipse.core.runtime.Status;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.Arrays;
import java.util.List;
import java.util.Map;

/**
 * @version $Revision: 494 $
 * @author <a href="mailto:dcorbin@users.sourceforge.net">David Corbin</a>
 * @author <a href="mailto:bpruessmann@users.sourceforge.net">Boris
 *         Pruessmann</a>
 */
public final class PerforceServer extends PlatformObject implements IPerforceServer {
  // ---------------------------------------------------------------------------
  // Private Constants

  /** The name of the password property. */
  public static final String INFO_PASSWORD = "net.sourceforge.perforce.core.password"; //$NON-NLS-1$
  /** The auth scheme. */
  public static final String AUTH_SCHEME = ""; //$NON-NLS-1$
  /** A faked url. */
  public static final URL FAKE_URL;

  static {
    URL temp = null;
    try {
      temp = new URL("http://net.sourceforge.perforce.core"); //$NON-NLS-1$
    } catch (MalformedURLException e) {
      // Ignored! May not happen.
    }
    FAKE_URL = temp;
  }

  // ---------------------------------------------------------------------------
  // Private Variables

  /** Stores a reference to the last result of p4 info. */
  private IInfoResult perforceInfoResult;
  /** Stores the P4PORT value. */
  private String port;
  /** Stores the P4CLIENT value. */
  private String client;
  /** Stores the P4HOST value. */
  private String host;
  /** Stores the P4USER value. */
  private String user;
  /** Stores the P4PASSWD value. */
  private String password;
  /** Stores the P4CHARSET value. */
  private CharSetEnum charset;
  /**
   * Indicates whether the server should be included in background updates or
   * not.
   */
  private boolean shouldIncludeInBackgroundUpdates;
  /** Stores the server address. */
  private String serverAddress;
  /** Stores the server root. */
  private String serverRoot;
  /** Stores the server date. */
  private String serverDate;
  /** Stores the server version. */
  private String serverVersion;
  /** Stores the server license. */
  private String serverLicense;
  /** Stores the client root */
  private IPath clientRoot;
  /** Stores the connection to the server. */
  private final Connection connection;
  /** Holds the id of the active changelist. */
  private Integer activeChangelist = IChangelist.DEFAULT;

  //----------------------------------------------------------------------------
  // ---- Constructors

  /**
   * Constructor for PerforceServer. <br>
   * <b>Only PerforceServerManager may create those ...</b>
   *
   * @param port value of P4PORT.
   * @param client value of P4CLIENT.
   * @param host value of P4HOST.
   * @param user value of P4USER.
   * @param password value of P4PASSWD.
   * @param charset value of P4CHARSET
   * @param backgroundUpdates include in background updates.
   */
  PerforceServer(String port, String client, String host, String user, String password,
      CharSetEnum charset, boolean backgroundUpdates) {
    this.port = port;
    this.client = client;
    this.host = host;
    this.user = user;
    this.password = password;
    this.charset = charset;
    this.connection = new Connection(this);
    shouldIncludeInBackgroundUpdates = backgroundUpdates;
    try {
      updateClientInfo();
    } catch (PerforceException e) {
      PerforceCorePlugin.logError("Exception setting working directory", e);
    }
  }

  // ------------------------------------------------------------ Public Methods
  // (IPerforceServer)

  /** @see IPerforceServer#members(IProgressMonitor) */
  public RemoteResource[] members(IProgressMonitor monitor) throws PerforceException {
    monitor = Policy.monitorFor(monitor);

    String[] depots = depots(monitor);

    List<RemoteResource> result = Lists.newArrayList();
    for (int i = 0; i < depots.length; i++) {
      FolderSyncInfo[] syncInfos = dirs(depots[i], monitor);
      if (syncInfos.length == 1) {
        result.add(new RemoteFolder(this, null, syncInfos[0], "#head"));
      }
    }

    return result.toArray(new RemoteResource[result.size()]);
  }

  public IFile getFile(String filename) throws PerforceException {
    IWorkspaceRoot root = PerforceCorePlugin.getWorkspace().getRoot();

    IWhereResult whereResult = where(filename);
    return root.getFile(new Path(whereResult.getLocalFilename()));
  }


  public String getUniqueName() {
    return "p4port=" + getPort() + ",p4client=" + getClient() + ",p4user=" + getUser();
  }

  public String getPort() {
    return port;
  }

  public void setPort(String thePort) {
    port = thePort;
  }

  public String getClient() {
    return client;
  }

  public void setClient(String theClient) {
    client = theClient;
  }

  public String getHost() {
    return host;
  }

  public void setHost(String theHost) {
    host = theHost;
  }

  public String getUser() {
    return user;
  }

  public void setUser(String theUser) {
    user = theUser;
  }

  public String getPassword() {
    if (password != null) {
      return password;
    }

    @SuppressWarnings("unchecked")
    Map map = Platform.getAuthorizationInfo(FAKE_URL, getUniqueName(), AUTH_SCHEME);
    if (map != null) {
      String password = (String) map.get(INFO_PASSWORD);
      if (password != null) {
        return password;
      }
    }

    return "";
  }

  public void setPassword(String passwd) {
    password = passwd;
  }

  public CharSetEnum getCharset() {
    return charset;
  }

  public void setCharset(CharSetEnum charset) {
    this.charset = charset;
  }

  public Integer change(ChangeSpec changeSpec) throws PerforceException {
    Change clientUser = Change.newInstanceForOutput(changeSpec.toString());
    run(new String[] {"-i"}, clientUser, null); //$NON-NLS-1$

    if (clientUser.getStatus().getSeverity() == IStatus.ERROR) {
      throw new PerforceException(clientUser.getStatus());
    }

    return (Integer) clientUser.getResult();
  }

  public IChangeResult change(Integer changelist) throws PerforceException {
    String[] arguments =
        changelist != null && changelist.intValue() != -1 ? new String[] {
            "-o", changelist.toString()} : //$NON-NLS-1$
            new String[] {"-o"}; //$NON-NLS-2$

    Change clientUser = Change.newInstanceForInput();

    run(arguments, clientUser, null);

    if (clientUser.getStatus().getSeverity() == IStatus.ERROR) {
      throw new PerforceException(clientUser.getStatus());
    }

    return (IChangeResult) clientUser.getResult();
  }

  public IChangesResult[] changes(String status, int count) throws PerforceException {
    Policy.assertTrue("pending".equals(status) || //$NON-NLS-1$
        "submitted".equals(status)); //$NON-NLS-1$

    String[] arguments = (count == Integer.MAX_VALUE) ? new String[] {"-l", "-s", status} : //$NON-NLS-1$ //$NON-NLS-2$
        new String[] {"-l", "-m", //$NON-NLS-1$ //$NON-NLS-2$
            Integer.toString(count), "-s", status}; //$NON-NLS-1$

    Changes clientUser = Changes.newInstance(this);
    run(arguments, clientUser, null);

    if (clientUser.getStatus().getSeverity() == IStatus.ERROR) {
      throw new PerforceException(clientUser.getStatus());
    }

    return clientUser.getResult();
  }

  public ICountersResult counters() throws PerforceException {
    Counters clientUser = Counters.newInstance();
    run(new String[0], clientUser, null);

    if (clientUser.getStatus().getSeverity() == IStatus.ERROR) {
      throw new PerforceException(clientUser.getStatus());
    }

    return clientUser.getResult();
  }

  public String[] depots(IProgressMonitor monitor) throws PerforceException {
    Depots clientUser = Depots.newInstance(monitor);
    run(new String[0], clientUser, monitor);

    if (clientUser.getStatus().getSeverity() == IStatus.ERROR) {
      throw new PerforceException(clientUser.getStatus());
    }

    return clientUser.getResult();
  }

  public IDescribeResult describe(Integer changelist) throws PerforceException {
    Describe clientUser = Describe.newInstance();
    run(new String[] {"-s", changelist.toString()}, clientUser, null);

    if (clientUser.getStatus().getSeverity() == IStatus.ERROR) {
      throw new PerforceException(clientUser.getStatus());
    }

    return clientUser.getResult();
  }

  public FolderSyncInfo[] dirs(String dir, IProgressMonitor monitor) throws PerforceException {
    Dirs clientUser = Dirs.newInstance(monitor);
    run(new String[] {"-C", dir}, clientUser, monitor); //$NON-NLS-1$

    if (clientUser.getStatus().getSeverity() == IStatus.ERROR) {
      throw new PerforceException(clientUser.getStatus());
    }

    return clientUser.getResult();
  }

  public P4Label[] labels() throws PerforceException {
    Labels clientUser = new Labels(this);
    run(new String[0], clientUser, null);

    if (clientUser.getStatus().getSeverity() == IStatus.ERROR) {
      throw new PerforceException(clientUser.getStatus());
    }

    return clientUser.getResult();
  }

  public P4Label[] labels(String fileSpec) throws PerforceException {
    Labels clientUser = new Labels(this);
    run(new String[] {fileSpec}, clientUser, null);

    if (clientUser.getStatus().getSeverity() == IStatus.ERROR) {
      throw new PerforceException(clientUser.getStatus());
    }

    return clientUser.getResult();
  }

  public IOpenedResult[] opened(IChangelist changelist) throws PerforceException {
    String changelistString =
        changelist.getId() == IChangelist.DEFAULT ? "default" : changelist.getId().toString(); //$NON-NLS-1$

    Opened clientUser = Opened.newInstance(this);
    run(new String[] {"-a", "-c", changelistString}, clientUser, null);//$NON-NLS-1$ //$NON-NLS-2$

    if (clientUser.getStatus().getSeverity() == IStatus.ERROR) {
      throw new PerforceException(clientUser.getStatus());
    }

    return clientUser.getResult();
  }

  public IOpenedResult[] opened(boolean allClients) throws PerforceException {
    String[] arguments = allClients ? new String[] {"-a"} : new String[0]; //$NON-NLS-1$

    Opened clientUser = Opened.newInstance(this);
    run(arguments, clientUser, null);

    if (clientUser.getStatus().getSeverity() == IStatus.ERROR) {
      throw new PerforceException(clientUser.getStatus());
    }

    return clientUser.getResult();
  }

  public IOpenedResult[] opened(IFile file) throws PerforceException {
    String[] arguments = {"-a", file.getLocation().toOSString()}; //$NON-NLS-1$

    Opened clientUser = Opened.newInstance(this);
    run(arguments, clientUser, null);

    if (clientUser.getStatus().getSeverity() == IStatus.ERROR) {
      throw new PerforceException(clientUser.getStatus());
    }

    return clientUser.getResult();
  }


  public void reopen(Integer changelist, IFileType fileType, IPath[] locations,
      IProgressMonitor monitor) throws PerforceException {
    IPerforceCommand command = Reopen.newInstance();

    List<String> arguments = Lists.newArrayListWithExpectedSize(locations.length + 4);
    if (changelist != null) {
      arguments.add("-c"); //$NON-NLS-1$
      arguments.add(changelist.intValue() == -1 ? "default" : changelist.toString()); //$NON-NLS-1$
    }

    if (fileType != null) {
      arguments.add("-t"); //$NON-NLS-1$
      arguments.add(fileType.toString());
    }

    for (int i = 0; i < locations.length; i++) {
      arguments.add(locations[i].toOSString());
    }

    try {
      monitor = Policy.monitorFor(monitor);
      monitor.beginTask("", locations.length * 100);
      run(arguments.toArray(new String[arguments.size()]), command, null);
    } finally {
      monitor.done();
    }

    if (command.getStatus().getSeverity() == IStatus.ERROR) {
      throw new PerforceException(command.getStatus());
    }
  }

  public IResolveResult[] resolve(String spec, ResolveTypeEnum resolveType, int options)
      throws PerforceException {
    Resolve clientUser = Resolve.newInstance();

    String[] arguments = buildResolveArguments(spec, resolveType, options);
    run(arguments, clientUser, null);

    if (clientUser.getStatus().getSeverity() == IStatus.ERROR) {
      throw new PerforceException(clientUser.getStatus());
    }

    return clientUser.getResult();
  }

  public IResolvedResult[] resolved(String filename) throws PerforceException {
    Resolved clientUser = Resolved.newInstance();
    if (filename != null) {
      run(new String[] {filename}, clientUser, null);
    } else {
      run(new String[0], clientUser, null);
    }

    if (clientUser.getStatus().getSeverity() == IStatus.ERROR) {
      throw new PerforceException(clientUser.getStatus());
    }

    return clientUser.getResult();
  }

  /** TODO: Refactor return value */
  public ICompositeSubmitResult submit(ChangeSpec changeSpec, IProgressMonitor monitor)
      throws PerforceException {
    ISubmitValidator[] validators = PerforceCorePlugin.getPlugin().getSubmitValidators();
    for (int i = 0; i < validators.length; i++) {
      IStatus status = validators[i].validateChangeSpec(this, changeSpec);
      if (!status.isOK()) {
        throw new PerforceException(status);
      }
    }

    Submit clientUser = Submit.newInstanceForOutput(changeSpec, monitor);
    run(new String[] {"-i"}, clientUser, null); //$NON-NLS-1$

    final IStatus status = clientUser.getStatus();
    if (status.isOK()) {
      setActiveChangelistId(IChangelist.DEFAULT);
    }

    final ISubmitResult[] result = clientUser.getResult();

    return new ICompositeSubmitResult() {
      public IStatus getStatus() {
        return status;
      }

      public ISubmitResult[] getResult() {
        return result;
      }
    };
  }

  public void revert(String[] fileNames, IProgressMonitor monitor) throws PerforceException {
    if (fileNames.length > 0) {
      simpleCmd(fileNames, "revert"); //$NON-NLS-1$
    }
  }

  public void delete(String[] fileNames) throws PerforceException {
    if (fileNames.length == 0) {
      return;
    }

    simpleCmd(Lists.asList("-c", getActiveChangelistString(), fileNames), "delete"); //$NON-NLS-1$
  }

  public void integrate(String from, String to) throws PerforceException {
    final String[] arguments = {"-c", //$NON-NLS-1$
        getActiveChangelistString(), from, to};

    simpleCmd(arguments, "integrate"); //$NON-NLS-1$
  }

  public void edit(String[] fileNames, final IProgressMonitor monitor) throws PerforceException {
    if (fileNames.length == 0) {
      return;
    }

    final MultiStatus multiStatus = new MultiStatus(PerforceCorePlugin.ID, IStatus.OK, "", null);

    IPerforceCommand command = new AbstractPerforceCommand(multiStatus) {
      @Override
      public void outputInfo(char level, String infoString) {
        super.outputInfo(level, infoString);
      }

      public String getCommand() {
        return "edit"; //$NON-NLS-1$
      }
    };

    final String[] arguments = new String[fileNames.length + 2];
    arguments[0] = "-c"; //$NON-NLS-1$
    arguments[1] = getActiveChangelistString();
    System.arraycopy(fileNames, 0, arguments, 2, fileNames.length);

    run(arguments, command, monitor);

    if (command.getStatus().getSeverity() == IStatus.ERROR) {
      throw new PerforceException(command.getStatus());
    }
  }

  public IStatus sync(String path, String revisionSpec, final IProgressMonitor monitor)
      throws PerforceException {
    return sync(path, revisionSpec, Lists.<IFile> newLinkedList(), monitor);
  }

  public IStatus sync(String path, String revisionSpec, List<IFile> result, IProgressMonitor monitor)
      throws PerforceException {
    Policy.assertNotEmpty(revisionSpec);

    SyncCommand command = SyncCommand.newInstance(monitor);
    if (!revisionSpec.equals("#head")) {
      run(new String[] {path + revisionSpec}, command, monitor);
    } else {
      run(new String[] {path}, command, monitor);
    }

    result.addAll(command.getResult());

    return command.getStatus();
  }

  /** @see IPerforceServer#sync(String, boolean, IProgressMonitor) */
  public String[] sync(String syncSpec, boolean simulate, final IProgressMonitor monitor)
      throws PerforceException {
    Policy.assertNotEmpty(syncSpec);

    final MultiStatus multiStatus = new MultiStatus(PerforceCorePlugin.ID, IStatus.OK, "", null);

    final List<String> files = Lists.newArrayList();

    IPerforceCommand command = new AbstractPerforceCommand(multiStatus) {
      @Override
      public void outputInfo(char level, String infoString) {
        if (level == '0') {
          int pos = infoString.indexOf('#');
          String filename = infoString.substring(0, pos);

          files.add(filename);
        }

        monitor.subTask(infoString);
      }

      public String getCommand() {
        return "sync";
      }
    };

    String[] arguments;
    if (simulate) {
      arguments = new String[] {"-n", syncSpec};
    } else {
      arguments = new String[] {syncSpec};
    }

    run(arguments, command, monitor);

    String[] filenames = files.toArray(new String[0]);
    try {
      IWhereResult[] whereResults = where(filenames);

      Policy.assertTrue(whereResults.length == filenames.length);

      for (int i = 0, length = whereResults.length; i < length; i++) {
        filenames[i] = whereResults[i].getLocalFilename();
      }
    } catch (PerforceException e) {
      PerforceCorePlugin.log(e.getStatus());
    }

    return filenames;
  }

  public InputStream print(String depotFileName, String revision) throws PerforceException {
    Policy.assertTrue(depotFileName != null && depotFileName.length() > 0);
    Policy.assertTrue(revision != null && revision.length() > 0);

    InputStream result = null;

    IWhereResult whereResult = where(depotFileName);
    depotFileName = whereResult.getClientFilename();

    try {
      File tempFile = File.createTempFile("p4-", ".tmp");
      tempFile.deleteOnExit();

      String[] arguments = new String[4];
      arguments[0] = "-o";
      arguments[1] = tempFile.getAbsolutePath();
      arguments[2] = "-q";
      arguments[3] = depotFileName + revision;

      Print clientUser = Print.newInstance();
      run(arguments, clientUser, null);

      if (clientUser.getStatus().getSeverity() == IStatus.ERROR) {
        throw new PerforceException(clientUser.getStatus());
      }

      return new FileInputStream(tempFile);
    } catch (IOException e) {
      throw new PerforceException(new Status(IStatus.ERROR, PerforceCorePlugin.ID, IStatus.ERROR,
          "PerforceServer.print failed.", e));
    }
  }

  public String[] diff(String[] files) throws PerforceException {
    if (files.length == 0) {
      return new String[0];
    }

    IWhereResult[] whereResults = where(files);
    for (int i = 0, length = files.length; i < length; i++) {
      files[i] = whereResults[i].getClientFilename();
    }

    Diff clientUser = Diff.newInstance();
    String[] arguments = new String[files.length + 1];
    arguments[0] = "-sr";
    System.arraycopy(files, 0, arguments, 1, files.length);

    run(arguments, clientUser, null);

    if (clientUser.getStatus().getSeverity() == IStatus.ERROR) {
      throw new PerforceException(clientUser.getStatus());
    }

    return clientUser.getResult();
  }

  public IWhereResult where(String filename) throws PerforceException {
    IWhereResult[] result = where(new String[] {filename});
    if (result.length == 0) {
      return null;
    }

    return result[0];
  }

  public IWhereResult[] where(String[] filenames) throws PerforceException {
    Where clientUser = Where.newInstance();
    String[] modifiedFileNames = new String[filenames.length];
    for (int i = 0; i < filenames.length; i++)
      modifiedFileNames[i] = Where.modifyFileName(filenames[i]);
    run(modifiedFileNames, clientUser, null);

    if (clientUser.getStatus().getSeverity() == IStatus.ERROR) {
      throw new PerforceException(clientUser.getStatus());
    }

    return clientUser.getResult();
  }

  public IWhereResult where(IResource resource) throws PerforceException {
    return where(resource.getLocation().toOSString());
  }

  public FolderRepositoryRelation determineFolderPosition(String folder) {
    try {
      if (folder.endsWith("/")) {
        folder = folder.substring(0, folder.length() - 1);
      }

      IPath folderPath = new Path(folder);
      IPath clientRootPath = getClientRoot();

      if (clientRootPath == null) {
        return null;
      }

      if (!EFS.getLocalFileSystem().isCaseSensitive()) {
        folderPath = new Path(folderPath.toOSString().toLowerCase());
        clientRootPath = new Path(clientRootPath.toOSString().toLowerCase());
      }

      if (clientRootPath.segmentCount() > 0) {
        if (!clientRootPath.isPrefixOf(folderPath)) {
          return FolderRepositoryRelation.OUTSIDE_ROOT;
        }
      }

      if (dirs(folder, null).length > 0) {
        return FolderRepositoryRelation.IN_REPOSITORY;
      }

      if (getDepotFileName(folder) != null) {
        return FolderRepositoryRelation.IN_VIEW;
      }

      return FolderRepositoryRelation.UNDER_ROOT;
    } catch (PerforceException pe) {
      PerforceCorePlugin.log(pe.getStatus());
      return null;
    }
  }

  public IClientForm getClientForm() throws PerforceException {
    Client clientUser = Client.newInstance();
    run(new String[] {"-o"}, clientUser, null);

    if (clientUser.getStatus().getSeverity() == IStatus.ERROR) {
      PerforceCorePlugin.log(clientUser.getStatus());
      return null;
    }

    return clientUser.getResult();
  }

  public boolean moveFile(IResourceTree tree, IFile sourceFile, IFile destinationFile,
      int updateFlags, IProgressMonitor monitor) {
    try {
      // wrap source and destination files
      LocalFile source = (LocalFile) LocalResource.createFromResource(sourceFile);
      LocalFile destination = (LocalFile) LocalResource.createFromResource(destinationFile);

      ResourceSyncInfo syncInfo = source.getSyncInfo();
      if (syncInfo.isNull()) {
        moveInFileSystem(tree, source, destination, updateFlags, monitor);
        return true;
      } else if (syncInfo.isAdded() || syncInfo.isBranched()) {
        IResolvedResult[] resolved = resolved(syncInfo.getClientFileName());

        // The file has already been integrated
        if (resolved.length == 1 && resolved[0].getAction().equals(ResourceSyncInfo.ACTION_BRANCH)) {
          String originalFile = getLocalFileName(resolved[0].getDepotFilename());
          String otherFile = getLocalFileName(destination.getAbsolutePath());

          // The new destination was the original source
          if (originalFile.equals(otherFile)) {
            moveToOriginalFile(tree, source, destination, updateFlags, monitor);
            return true;
          } else {
            moveResolvedFile(tree, source, destination, updateFlags, originalFile, monitor);
            return true;
          }
        } else {
          moveNewlyAddedFile(tree, source, destination, updateFlags, monitor);
          return true;
        }
      } else {
        moveUnopenedFile(tree, source, destination, updateFlags, monitor);
        return true;
      }
    } catch (PerforceException e) {
      try {
        revert((LocalFile) LocalFile.createFromResource(destinationFile));

        File file = new File(destinationFile.getLocation().toOSString());
        if (file.exists()) {
          file.delete();
        }
      } catch (PerforceException ee) {
        PerforceCorePlugin.log(e.getStatus());
      }

      tree.failed(e.getStatus());
      return false;
    }
  }

  public String getServerAddress() throws PerforceException {
    if (serverAddress == null) {
      updateServerInfo();
    }

    return serverAddress;
  }

  public String getServerRoot() throws PerforceException {
    if (serverRoot == null) {
      updateServerInfo();
    }

    return serverRoot;
  }

  public String getServerDate() throws PerforceException {
    if (serverDate == null) {
      updateServerInfo();
    }

    return serverDate;
  }

  public String getServerVersion() throws PerforceException {
    if (serverVersion == null) {
      updateServerInfo();
    }

    return serverVersion;
  }

  public String getServerLicense() throws PerforceException {
    if (serverLicense == null) {
      updateServerInfo();
    }

    return serverLicense;
  }

  public void validateConnection(IProgressMonitor monitor) throws PerforceException {
    try {
      Connection connection = new Connection(this);
      try {
        connection.open(monitor);
      } finally {
        connection.close();
      }
      monitor.worked(1);


      IInfoResult result = info(monitor);
      monitor.worked(1);

      if (result.getClientName() == null || !result.getClientName().equals(getClient())
          || result.getClientRoot() == null) {
        throw new PerforceException(new Status(Status.ERROR, PerforceCorePlugin.ID, 0, Policy
            .bind("PerforceServer.invalidClient"), null));
      }

      if (result.getUserName() == null || !result.getUserName().equals(getUser())) {
        throw new PerforceException(new Status(Status.ERROR, PerforceCorePlugin.ID, 0, Policy
            .bind("RepositoryLocation.invalidUser"), null));
      }

      // we perform depots() to find out whether the password is okay or not.
      try {
        String[] depots = depots(null);
      } catch (PerforceException e) {
        IStatus status = e.getStatus();
        status = status.getChildren()[0];

        throw new PerforceException(status);
      }

    } catch (PerforceException e) {
      throw e;
    }
  }

  public ILogEntry[] filelog(RemoteFile remoteFile, int maxRevs, boolean follow)
      throws PerforceException {
    String[] arguments;
    if (follow) {
      arguments =
          new String[] {"-i", "-l", "-m", Integer.toString(maxRevs), remoteFile.getDepotFilename()};
    } else {
      arguments =
          new String[] {"-l", "-m", Integer.toString(maxRevs), remoteFile.getDepotFilename()};
    }

    Filelog filelogUser = Filelog.newInstance(this, new NullProgressMonitor());
    run(arguments, filelogUser, null);

    if (filelogUser.getStatus().getSeverity() == IStatus.ERROR) {
      throw new PerforceException(filelogUser.getStatus());
    }

    return filelogUser.getResult();
  }

  public RemoteFile[] findAllFiles(String filespec, IProgressMonitor monitor)
      throws PerforceException {
    List<RemoteFile> result = Lists.newArrayList();

    Fstat clientUser = Fstat.newInstance(monitor);
    run(new String[] {"-C", "//.../" + filespec}, clientUser, monitor);

    ResourceSyncInfo[] syncInfos = clientUser.getResult();
    for (int i = 0; i < syncInfos.length; i++) {
      if (syncInfos[i].getHeadAction().equals("delete")) continue;
      result.add(new RemoteFile(this, syncInfos[i].getDepotFileName(), "#head"));
    }

    return result.toArray(new RemoteFile[result.size()]);
  }

  public RemoteResource find(String name, IProgressMonitor monitor) throws PerforceException {
    RemoteResource[] children = members(monitor);
    for (int i = 0, length = children.length; i < length; i++) {
      if (name.startsWith(children[i].getDepotFilename())) {
        return children[i].find(name, monitor);
      }
    }

    return null;
  }

  static Profiler profiler = new Profiler("PerforceSver.fstat");

  public ResourceSyncInfo[] fstat(String file, IProgressMonitor monitor) throws PerforceException {
    profiler.exec(file);
    Fstat clientUser = Fstat.newInstance(monitor);
    run(new String[] {"-C", file}, clientUser, monitor); //$NON-NLS-1$

    if (clientUser.getStatus().getSeverity() == IStatus.ERROR) {
      throw new PerforceException(clientUser.getStatus());
    }

    return clientUser.getResult();
  }

  public ResourceSyncInfo[] fstat(IFile[] files, IProgressMonitor monitor) throws PerforceException {
    String[] arguments = new String[files.length + 1];
    arguments[0] = "-C";
    for (int i = 0; i < files.length; i++) {
      profiler.exec(files[i]);
      arguments[i + 1] = files[i].getLocation().toOSString();
    }

    Fstat clientUser = Fstat.newInstance(monitor);
    run(arguments, clientUser, monitor);

    if (clientUser.getStatus().getSeverity() == IStatus.ERROR) {
      throw new PerforceException(clientUser.getStatus());
    }

    ResourceSyncInfo[] tempResult = clientUser.getResult();
    ResourceSyncInfo[] result = new ResourceSyncInfo[files.length];
    int j = 0;
    for (int i = 0; i < files.length; i++) {
      final ResourceSyncInfo syncInfo = j < tempResult.length ? tempResult[j] : null;
      if (syncInfo != null
          && syncInfo.getClientFileName().equalsIgnoreCase(files[i].getLocation().toOSString())) {
        result[i] = tempResult[j++];
      } else {
        result[i] = null;
      }
    }

    return result;
  }

  public synchronized IInfoResult info(IProgressMonitor monitor) throws PerforceException {
    if (perforceInfoResult == null) {
      Info clientUser = Info.newInstance(Policy.monitorFor(monitor));
      run(new String[0], clientUser, monitor);

      perforceInfoResult = clientUser.getResult();
    }

    return perforceInfoResult;
  }

  public void add(String[] fileNames, IProgressMonitor monitor) throws PerforceException {
    if (fileNames.length == 0) {
      return;
    }
    List<String> directAdd = Lists.newArrayList();
    boolean adjustFileNames = false;
    for (int i = 0; i < fileNames.length; i++) {
      ResourceSyncInfo[] infos = fstat(fileNames[i], null);
      if (infos == null || infos.length == 0) continue;
      if (infos.length != 1) {
        Status status =
            new Status(IStatus.ERROR, PerforceCorePlugin.ID, 0, "Error checking for Re-Add "
                + fileNames[i], null);
        throw new PerforceException(status);
      }
      if (infos[0].getAction().equals(ResourceSyncInfo.ACTION_DELETE)) {
        try {
          revertAndEdit(fileNames[i], monitor);
          adjustFileNames = true;
        } catch (CoreException e) {
          PerforceCorePlugin.logError("Error re-adding " + fileNames[i], e);
          throw new PerforceException(e.getStatus());
        } catch (Exception pe) {
          Status status =
              new Status(IStatus.ERROR, PerforceCorePlugin.ID, 0,
                  "Error re-adding " + fileNames[i], pe);
          throw new PerforceException(status);
        }
      } else {
        directAdd.add(fileNames[i]);
      }


      if (adjustFileNames) fileNames = directAdd.toArray(new String[0]);
    }
    if (fileNames.length == 0) return;

    final String[] arguments = new String[fileNames.length + 2];
    arguments[0] = "-c"; //$NON-NLS-1$
    arguments[1] = getActiveChangelistString();
    System.arraycopy(fileNames, 0, arguments, 2, fileNames.length);

    simpleCmd(arguments, "add");
  }

  private void revertAndEdit(String fileName, IProgressMonitor monitor) throws Exception {
    File file = new File(fileName);
    File backup = null;
    try {
      backup = findBackup(file);
      file.renameTo(backup);
    } catch (IOException e) {
      String msg = "Unable to find temporary backup for " + fileName;
      PerforceCorePlugin.logError(msg, e);
      Status status = new Status(IStatus.ERROR, PerforceCorePlugin.ID, 0, msg, e);
      throw new PerforceException(status);
    }
    try {
      revert(new String[] {fileName}, monitor);
      edit(new String[] {fileName}, monitor);
    } finally {
      if (file.exists()) file.delete();
      backup.renameTo(file);
    }
  }

  private File findBackup(File file) throws IOException {
    File backup = new File(file.getCanonicalPath() + ".p4ebak");
    int counter = 0;
    while (backup.exists()) {
      backup = new File(file.getCanonicalPath() + ".p4ebak" + counter);
      counter++;
    }
    return backup;
  }

  /**
   * Call this method to retrieve the connection object.
   *
   * @return the connection object.
   */
  public Connection getConnection() {
    return connection;
  }

  /**
   * Called to update the password cache.
   *
   * @throws PerforceException in case of an unexpected error.
   */
  public void updateCache() throws PerforceException {
    updateCache(user, password, true);
    password = null;
  }

  public boolean shouldIncludeInBackgroundUpdates() {
    return shouldIncludeInBackgroundUpdates;
  }

  public void setShouldIncludeInBackgroundUpdates(boolean value) {
    shouldIncludeInBackgroundUpdates = value;
  }

  public void deleteChangelist(IChangelist changelist) throws PerforceException {
    Change clientUser = Change.newInstanceForInput();
    run(new String[] {"-d", changelist.getId().toString()}, clientUser, null); //$NON-NLS-1$

    if (changelist.getId().equals(activeChangelist)) {
      setActiveChangelistId(IChangelist.DEFAULT);
    }

    if (clientUser.getStatus().getSeverity() == IStatus.ERROR) {
      throw new PerforceException(clientUser.getStatus());
    }
  }

  public Integer getActiveChangelistId() {
    return activeChangelist;
  }

  public void setActiveChangelistId(Integer changelistId) {
    Policy.assertNotNull(changelistId);

    activeChangelist = changelistId;
    PerforceCorePlugin.getPlugin().broadcastChangelistChanges();
  }

  /** Called to invalidate all known connections. */
  public synchronized void invalidateConnections() {
    connection.invalidate();
  }

  //----------------------------------------------------------------------------
  // - Private Methods

  /**
   * Retrieves the depot pathname of the specified path.
   *
   * @param path the input path.
   * @return the path in depot syntax.
   *
   * @throws PerforceException in case of an error.
   */
  private String getDepotFileName(String path) throws PerforceException {
    IWhereResult whereResult = where(path);
    return (whereResult != null) ? whereResult.getDepotFilename() : null;
  }

  /**
   * Retrieves the local filename of the specified path.
   *
   * @param path the input path.
   * @return the path in depot syntax.
   *
   * @throws PerforceException in case of an error.
   */
  private String getLocalFileName(String path) throws PerforceException {
    IWhereResult whereResult = where(path);
    return whereResult.getLocalFilename();
  }

  /**
   * Returns the path of the given file in form of an array.
   *
   * @param file the file
   * @return array[1] containing the path.
   */
  private String[] fileNames(LocalFile file) {
    return new String[] {file.getAbsolutePath()};
  }

  private void revert(LocalFile[] files) throws PerforceException {
    revert(LocalFile.fileNames(files), null);
  }

  private void revert(LocalFile file) throws PerforceException {
    revert(new LocalFile[] {file});
  }

  private void add(LocalFile[] files) throws PerforceException {
    add(LocalFile.fileNames(files), null);
  }

  private void add(LocalFile file) throws PerforceException {
    add(new LocalFile[] {file});
  }

  private void edit(LocalFile[] files) throws PerforceException {
    edit(LocalFile.fileNames(files), new NullProgressMonitor());
  }

  private void edit(LocalFile file) throws PerforceException {
    edit(new LocalFile[] {file});
  }

  private void delete(LocalFile file) throws PerforceException {
    delete(fileNames(file));
  }

  /**
   * Performs Perforce move for files that are not currently opened for ..
   *
   * @param tree the resource tree.
   * @param source the source of the operation.
   * @param destination the destination.
   * @param monitor for progress feedback.
   *
   * @throws PerforceException in case of an error.
   */
  private void moveUnopenedFile(IResourceTree tree, LocalFile source, LocalFile destination,
      int updateFlags, IProgressMonitor monitor) throws PerforceException {
    integrate(source.getAbsolutePath(), destination.getAbsolutePath());

    if (source.isCheckedOut()) {
      edit(destination);

      moveInFileSystem(tree, source, destination, updateFlags, monitor);

      revert(source);
      delete(source);
    } else {
      delete(source);
    }
  }

  /**
   * Performs perforce move for a file that has been newly added to Perforce,
   * but not via integrate.
   *
   * @param tree the resource tree.
   * @param source the source of the operation.
   * @param destination the destination.
   * @param monitor for progress feedback.
   *
   * @throws PerforceException in case of an error.
   */
  private void moveNewlyAddedFile(IResourceTree tree, LocalFile source, LocalFile destination,
      int updateFlags, IProgressMonitor monitor) throws PerforceException {
    moveInFileSystem(tree, source, destination, updateFlags, monitor);

    add(destination);
    revert(source);
  }

  /**
   * Performs a move in which the destination is the file from which the
   * original rename/integrate cycle started.
   *
   * @param tree the resource tree.
   * @param source the source of the operation.
   * @param destination the destination.
   * @param monitor for progress feedback.
   *
   * @throws PerforceException in case of an error
   */
  private void moveToOriginalFile(IResourceTree tree, LocalFile source, LocalFile destination,
      int updateFlags, IProgressMonitor monitor) throws PerforceException {
    revert(destination);
    if (!source.isCheckedOut()) {
      revert(source);
    } else {
      edit(destination);
      moveInFileSystem(tree, source, destination, updateFlags, monitor);
      revert(source);
    }
  }

  /**
   * Performs a Perforce move for a file that has already been integrated.
   *
   * @param tree the resource tree.
   * @param source the source of the operation.
   * @param destination the destination.
   * @param monitor for progress feedback.
   * @param resolved the original file path.
   *
   * @throws PerforceException in case of an error.
   */
  private void moveResolvedFile(IResourceTree tree, LocalFile source, LocalFile destination,
      int updateFlags, String resolved, IProgressMonitor monitor) throws PerforceException {
    integrate(resolved, destination.getAbsolutePath());
    if (!source.isCheckedOut()) {
      revert(source);
    } else {
      edit(destination);

      moveInFileSystem(tree, source, destination, updateFlags, monitor);

      revert(source);
    }
  }

  /**
   * Performs a simple file base Perforce operation.
   *
   * @param fileNames array of file names.
   * @param cmd the command.
   *
   * @throws PerforceException in case of an error.
   */
  private void simpleCmd(String[] fileNames, String cmd) throws PerforceException {
    SimpleCmd clientUser = SimpleCmd.newInstance(cmd);
    run(fileNames, clientUser, null);

    if (clientUser.getStatus().getSeverity() == IStatus.ERROR) {
      throw new PerforceException(clientUser.getStatus());
    }
  }

  /**
   * Performs a simple file base Perforce operation.
   *
   * @param fileNames array of file names.
   * @param cmd the command.
   *
   * @throws PerforceException in case of an error.
   */
  private void simpleCmd(List<String> fileNames, String cmd) throws PerforceException {
    SimpleCmd clientUser = SimpleCmd.newInstance(cmd);
    run(fileNames.toArray(new String[fileNames.size()]), clientUser, null);

    if (clientUser.getStatus().getSeverity() == IStatus.ERROR) {
      throw new PerforceException(clientUser.getStatus());
    }
  }

  /**
   * Returns the current client's perforce root.
   *
   * @return the client root.
   *
   * @throws PerforceException in case of an unexpected error.
   */
  public IPath getClientRoot() throws PerforceException {
    if (clientRoot == null) {
      updateClientInfo();
    }

    return clientRoot;
  }

  /**
   * Called to update the internal info about the server.
   *
   * @throws PerforceException in case of an error-
   */
  private void updateServerInfo() throws PerforceException {
    IInfoResult result = info(null);

    serverAddress = result.getServerAddress();
    serverRoot = result.getServerRoot();
    serverDate = result.getServerDate();
    serverVersion = result.getServerVersion();
    serverLicense = result.getServerLicense();
  }

  /**
   * Called to update the internal info about the client.
   *
   * @throws PerforceException in case of an unexpected error.
   */
  private void updateClientInfo() throws PerforceException {
    IInfoResult result = info(null);
    if (result.getClientRoot() == null) {
      clientRoot = null;
    } else if (result.getClientRoot().equalsIgnoreCase("null")) {
      clientRoot = Path.EMPTY;
    } else {
      clientRoot = new Path(result.getClientRoot());
    }
    if (clientRoot != null) {
      connection.setClientRoot(clientRoot);
    }
  }

  /**
   * Runs the given command with the specified arguments.
   *
   * @param arguments the arguments of the operation.
   * @param command the command to execute.
   * @param monitor monitor for progress feedback.
   *
   * @throws PerforceException in case of an unexpected error.
   */
  private void run(String[] arguments, IPerforceCommand command, IProgressMonitor monitor)
      throws PerforceException {
    for (int i = 0; i < arguments.length; i++) {
      Preconditions.checkNotNull(arguments[i], "Null object found in command: %s, args: [%s]",
          command.getCommand(), Arrays.toString(arguments));
    }

    monitor = Policy.monitorFor(monitor);
    try {
      synchronized (connection) {
        connection.open(monitor);
        connection.run(arguments, command, monitor);
      }
    } finally {
      monitor.done();
    }
  }

  /**
   * Cache the user info in the keyring.
   *
   * @param user the username.
   * @param password the password.
   * @param createIfAbsent indicates whether a new entry should be created when
   *        needed.
   *
   * @throws PerforceException in case of an error.
   */
  @SuppressWarnings("unchecked")
  // map
  private void updateCache(String user, String password, boolean createIfAbsent)
      throws PerforceException {
    Map map = Platform.getAuthorizationInfo(FAKE_URL, getUniqueName(), AUTH_SCHEME);
    if (map == null) {
      if (!createIfAbsent) {
        return;
      }

      map = Maps.newHashMapWithExpectedSize(1);
    }

    if (password != null) {
      map.put(INFO_PASSWORD, password);
    }

    try {
      Platform.addAuthorizationInfo(FAKE_URL, getUniqueName(), AUTH_SCHEME, map);
    } catch (CoreException e) {
      PerforceCorePlugin.log(e.getStatus());
      throw new PerforceException(new Status(Status.ERROR, PerforceCorePlugin.ID, Status.ERROR,
          "Error", e));
    }
  }

  /**
   * Builds the argument array for the resolve operation.
   *
   * @param spec the file spec to resolve.
   * @param resolveType the resolve type.
   * @param options misc options to the resolve command.
   * @return argument array.
   */
  private String[] buildResolveArguments(String spec, ResolveTypeEnum resolveType, int options) {
    // ==( PRE-CONDITION
    // )======================================================================
    Policy.assertTrue((options & ~RESOLVE_OPTIONS) == 0);
    // ==( PRE-CONDITION
    // )======================================================================

    List<String> argumentList = Lists.newArrayList();
    if (resolveType == ResolveTypeEnum.ACCEPT_THEIRS) {
      argumentList.add("-at"); //$NON-NLS-1$
    } else if (resolveType == ResolveTypeEnum.ACCEPT_YOURS) {
      argumentList.add("-ay"); //$NON-NLS-1$
    } else if (resolveType == ResolveTypeEnum.SAFE_AUTOMATIC) {
      argumentList.add("-as"); //$NON-NLS-1$
    } else if (resolveType == ResolveTypeEnum.AUTOMATIC) {
      argumentList.add("-am"); //$NON-NLS-1$
    } else if (resolveType == ResolveTypeEnum.AUTOMATIC_MARKERS) {
      argumentList.add("-af"); //$NON-NLS-1$
    }

    if ((options & RESOLVE_FORCE) > 0) {
      argumentList.add("-f"); //$NON-NLS-1$
    }

    if ((options & RESOLVE_MERGE_BINARY) > 0) {
      argumentList.add("-t"); //$NON-NLS-1$
    }

    if ((options & RESOLVE_SIMULATE) > 0) {
      argumentList.add("-n"); //$NON-NLS-1$
    }

    if (spec != null) {
      argumentList.add(spec);
    }
    return argumentList.toArray(new String[argumentList.size()]);
  }

  private void moveInFileSystem(IResourceTree tree, LocalFile source, LocalFile destination,
      int updateFlags, IProgressMonitor monitor) throws PerforceException {
    IFile src = Helper.getIFileForLocalFile(source);
    IPath dest = Helper.getIPathForLocalFile(destination);
    try {
      moveInFileSystem(src, dest, updateFlags, monitor);
    } catch (CoreException e) {
      tree.failed(e.getStatus());
      throw new PerforceException(e.getStatus());
    }
  }

  /**
   * Move the contents of the specified file from the source location to the
   * destination location. If the source points to a directory then move that
   * directory and all its contents.
   *
   * {@code IResource.FORCE} is the only valid flag.
   */
  private void moveInFileSystem(IResource source, IPath destination, int updateFlags,
      IProgressMonitor monitor) throws CoreException {
    source.move(destination, updateFlags, monitor);
  }

  /**
   * Called to retrieve the changelist string of the currently active
   * changelist.
   *
   * @return the string for the currently active changelist.
   */
  private String getActiveChangelistString() {
    if (activeChangelist.equals(IChangelist.DEFAULT)) {
      return "default"; //$NON-NLS-1$
    } else {
      return activeChangelist.toString();
    }
  }
}
